package com.astersoft.lib.db;

/*
 * Copyright 2012 AsterSoft personal developer - Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
 * or agreed to in writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Date;
import java.util.List;

import javax.persistence.Transient;

import com.astersoft.lib.db.annotation.Column;
import com.astersoft.lib.util.ASLists;

/**
 * Class that provide automatic mapping of table data to Java POJO.
 * 
 * This class provide a singleton access and automatic mapping based on the
 * {@link Column} annotation
 * 
 * @author dario durzo
 * @version 0.1
 * @date 29-05-2012
 * 
 * @param <T>
 */
public class ASResultsetMapper<T> {
	@SuppressWarnings("rawtypes")
	private static ASResultsetMapper _instance;
	private ASMapperDefaultValues _mapperDefaults = new ASMapperDefaultValues() {
		@Override
		public Object doFilter(Object o) {
			return o;
		}
	};

	/**
	 * Singleton instance provider
	 * 
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public static <T> ASResultsetMapper<T> getInstance() {
		if (_instance == null) {
			synchronized (ASResultsetMapper.class) {
				if (_instance == null)
					_instance = new ASResultsetMapper<T>();
			}
		}
		return _instance;
	}

	private ASResultsetMapper() {}

	public ASMapperDefaultValues getDefaultMapper() {
		return this._mapperDefaults;
	}

	/**
	 * Change the default mapper
	 * 
	 * @see ASMapperDefaultValues
	 * @param map
	 */
	public void changeDefaultMapper(ASMapperDefaultValues map) {
		this._mapperDefaults = map;
	}

	/**
	 * Main convert method that map a ResultSet object to
	 * a POJO class object
	 * 
	 * @param rs
	 * 			the ResultSet
	 * @param toClazz
	 * 			the result class (POJO OBJECT)
	 * @return
	 * 			the object filled
	 * @throws ASMapperException
	 */
	public T toObject(ResultSet rs, Class<T> toClazz)
	throws ASMapperException {
		T obj = null;
		Field[] fields = toClazz.getDeclaredFields();
		try {
			if (rs.next() && fields != null && fields.length > 0) {
				obj = toClazz.newInstance();
				populateFields(rs, obj, fields);
			}
		} catch (Exception e) {
			e.printStackTrace();
			throw new ASMapperException(e);
		}

		return obj;
	}

	/**
	 * Method that maps a ResultSet in a list of
	 * POJO object 
	 * 
	 * @param rs
	 * 			the ResultSet
	 * @param toClazz
	 * 			the result class (POJO OBJECT)
	 * @return
	 * 			the object filled
	 * @throws ASMapperException
	 */
	public List<T> toList(ResultSet rs, Class<T> toClazz)
	throws ASMapperException {
		List<T> listObj = ASLists.newLinkedList();
		T obj = null;
		Field[] fields = toClazz.getDeclaredFields();
		if (fields == null || fields.length == 0)
			return listObj;
		try {
			while (rs.next()) {
				obj = toClazz.newInstance();
				populateFields(rs, obj, fields);
				listObj.add(obj);
			}
		} catch (Exception e) {
			e.printStackTrace();
			throw new ASMapperException(e);
		}

		return listObj;
	}

	/**
	 * Internal method to map ResultSet field with 
	 * POJO object field based on name or Column annotation
	 * 
	 * @param rs
	 * 			The ResultSet object
	 * @param obj
	 * 			The POJO object
	 * @param fields
	 * 			The fields definition
	 * @throws IllegalAccessException
	 * @throws SQLException
	 */
	private void populateFields(ResultSet rs, T obj, Field[] fields)
	throws IllegalAccessException, SQLException {
		String fieldName = null;
		for (Field f : fields) {
			if ("serialVersionUID".equals(f.getName()))
				continue;
			f.setAccessible(true);
			fieldName = f.getName();
			Class<?> fieldType = f.getType();
			Annotation[] annotations = f.getDeclaredAnnotations();

			if (annotations != null && annotations.length > 0) {
				if (annotations[0].annotationType() == Column.class) {
					Column colAnn = (Column) annotations[0];
					fieldName = colAnn.name();
					if (colAnn.type() != Object.class)
						fieldType = colAnn.type();
				} else if (annotations[0].annotationType() == Transient.class)
					continue;
			}
			
			// check if column exist or is to ignore
			try {
				rs.findColumn(fieldName);
			} catch(SQLException se) {
				continue;
			}

			// go ahead
			if (fieldType == Long.class)
				f.set(obj, getCustomMapping(Long.class, Long.valueOf(rs.getLong(fieldName))));

			if (fieldType == String.class)
				f.set(obj, getCustomMapping(String.class, rs.getString(fieldName)));

			if (fieldType == Integer.class)
				f.set(obj, getCustomMapping(Integer.class, Integer.valueOf(rs.getInt(fieldName))));

			if (fieldType == Double.class)
				f.set(obj, getCustomMapping(Double.class, Double.valueOf(rs.getDouble(fieldName))));

			if (fieldType == Date.class) {
				java.sql.Date sqlDate = rs.getDate(fieldName);
				if (sqlDate != null)
					f.set(obj, new java.util.Date(sqlDate.getTime()));
				else
					f.set(obj, getCustomMapping(java.util.Date.class, sqlDate));
			}
			if (fieldType == BigDecimal.class)
				f.set(obj, getCustomMapping(BigDecimal.class, rs.getBigDecimal(fieldName)));

			if (fieldType == Boolean.class)
				f.set(obj, getCustomMapping(Boolean.class, Boolean.valueOf(rs.getBoolean(fieldName))));

			if (fieldType == Float.class)
				f.set(obj, getCustomMapping(Float.class, Float.valueOf(rs.getFloat(fieldName))));

			if (fieldType == Short.class)
				f.set(obj, getCustomMapping(Short.class, Short.valueOf(rs.getShort(fieldName))));

			if (fieldType == Timestamp.class)
				f.set(obj, getCustomMapping(Timestamp.class, rs.getTimestamp(fieldName)));
		}
	}

	/**
	 * Retrieve a custom mapping applying 
	 * eventually defined filter for specific field
	 * 
	 * @param cl
	 * 			The reference class
	 * @param obj
	 * 			The object instance
	 * @return
	 */
	private Object getCustomMapping(Class<?> cl, Object obj) {
		if (obj == null)
			return this._mapperDefaults.getCustomMapping(cl);
		return this._mapperDefaults.doFilter(obj);
	}
}
